﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace CygwinPackageManager.Common.Business
{
	public partial class PackageManager : Component
	{
		#region LifeCycle
		public PackageManager()
		{
			InitializeComponent();
		}

		public PackageManager(IContainer container)
		{
			container.Add(this);

			InitializeComponent();
		}
		#endregion

		#region Public Properties
		private IEnumerable<PackageStatus> _packageStatuses;
		[Browsable(false)]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public IEnumerable<PackageStatus> PackageStatuses
		{
			get
			{
				if (_packageStatuses == null)
					_packageStatuses = (new PackageStatusDALC()).GetStatuses();
				return _packageStatuses;
			}
			private set { _packageStatuses = value; }
		}

		private IEnumerable<PackageCategory> _packageCategories;
		[Browsable(false)]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public IEnumerable<PackageCategory> PackageCategories
		{
			get
			{
				if (_packageCategories == null)
					_packageCategories = (new PackageCategoryDALC()).GetCategories();
				return _packageCategories;
			}
			private set { _packageCategories = value; }
		}

		private IEnumerable<AvailablePackage> _availablePackages;
		[Browsable(false)]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public IEnumerable<AvailablePackage> AvailablePackages
		{
			get
			{
				if (_availablePackages == null)
					_availablePackages = (new AvailablePackageDALC()).GetAvailablePackages();
				return _availablePackages;
			}
			private set { _availablePackages = value; }
		}

		private IEnumerable<InstalledPackage> _installedPackages;
		[Browsable(false)]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public IEnumerable<InstalledPackage> InstalledPackages
		{
			get
			{
				if (_installedPackages == null)
					_installedPackages = (new InstalledPackageDALC()).GetInstalledPackages();
				return _installedPackages;
			}
		}
		#endregion

		#region Private Methods
		private IEnumerable<PackageInfo> _GetPackageInfos()
		{
			IEnumerable<AvailablePackage> availablePackages = AvailablePackages;
			IEnumerable<InstalledPackage> installedPackages = InstalledPackages;
			PackageCategoryDALC categoryDALC = new PackageCategoryDALC();

			List<PackageInfo> packageInfos = new List<PackageInfo>();

			// Build package info list.
			foreach (AvailablePackage availablePackage in availablePackages)
			{
				int packageID = availablePackage.PackageID;

				// Status and Version.
				string availablePackageVersion = availablePackage.VendorVersion;

				string releaseVersion = availablePackage.ReleaseVersion;
				if (!string.IsNullOrEmpty(releaseVersion))
				{
					if (!string.IsNullOrEmpty(availablePackageVersion))
						availablePackageVersion += string.Concat("-", releaseVersion);
					else
						availablePackageVersion = releaseVersion;
				}

				PackageStatusIndex status = PackageStatusIndex.None;
				InstalledPackage installedPackage = installedPackages.FirstOrDefault(p => p.PackageID == packageID);
				if (installedPackage == null)
				{
					status = PackageStatusIndex.Available;
				}
				else
				{
					string installedPackageVersion = string.Format("{0}-{1}", installedPackage.VendorVersion, installedPackage.ReleaseVersion);
					if (availablePackageVersion == installedPackageVersion)
					{
						status = PackageStatusIndex.InstalledUpdated;
					}
					else
					{
						// Assume available package version is greater than installed.
						status = PackageStatusIndex.InstalledOutdated;
					}
				}

				// Categories
				List<PackageCategory> categories = new List<PackageCategory>();
				foreach (string categoryName in availablePackage.Categories)
				{
					int categoryID = categoryDALC.GetCategoryID(categoryName);
					PackageCategory category = PackageCategories.Single(c => c.PackageCategoryID == categoryID);
					categories.Add(category);
				}

				packageInfos.Add(new PackageInfo()
				{
					Text = availablePackage.Text,
					Version = availablePackageVersion,
					PackageID = packageID,
					StatusID = (int)status,
					CurrentStatusID = (int)status,
					Name = availablePackage.Name,
					Description = availablePackage.Description,
					Details = availablePackage.Details,
					Categories = categories,
					IsSpecial = availablePackage.IsSpecial,
				});
			}

			// Requirements.
			foreach (PackageInfo packageInfo in packageInfos)
			{
				AvailablePackage availablePackage = availablePackages.Single(p => p.PackageID == packageInfo.PackageID);

				List<PackageInfo> requirs = new List<PackageInfo>();
				foreach (string require in availablePackage.Requires)
				{
					if (string.IsNullOrEmpty(require))
						continue;

					int requiredPackageID = AvailablePackageDALC.GetPackageID(require);
					// HACK: libfpx1-devel is listed only in requirements of libMagick-devel but does not exist as a package..
					PackageInfo requiredPackage = packageInfos.SingleOrDefault(p => p.PackageID == requiredPackageID);
					if (requiredPackage != null)
						requirs.Add(requiredPackage);
				}
				packageInfo.Requires = requirs;
			}

			return packageInfos;
		}
		#endregion

		#region Private Properties
		private IEnumerable<PackageInfo> _packageInfos;
		private IEnumerable<PackageInfo> PackageInfos
		{
			get
			{
				if (_packageInfos == null)
					_packageInfos = _GetPackageInfos();

				return _packageInfos;
			}
			set { _packageInfos = value; }
		}
		#endregion

		#region Public Methods
		public PackageInfo GetPackageByID(int packageID)
		{
			if (packageID < 1)
				throw new ArgumentException();

			IEnumerable<PackageInfo> packageInfos = PackageInfos;
			if (packageInfos == null)
				throw new InvalidOperationException();

			return packageInfos.Single(p => p.PackageID == packageID);
		}

		public IEnumerable<PackageInfo> GetPackagesByID(IEnumerable<int> packageIDs)
		{
			if (packageIDs == null)
				throw new ArgumentException();

			IEnumerable<PackageInfo> packageInfos = PackageInfos;
			if (packageInfos == null)
				throw new InvalidOperationException();

			return from package in packageInfos
				   where packageIDs.Contains(package.PackageID)
				   select package;
		}

		public IEnumerable<PackageInfo> FindPackages(string freeText, IEnumerable<int> selectedStatusIDs, IEnumerable<int> selectedCategoryIDs)
		{
			IEnumerable<PackageInfo> packages = PackageInfos;
			if (packages == null)
				throw new InvalidOperationException();

			Func<PackageInfo, bool> predicate = new Func<PackageInfo, bool>(delegate(PackageInfo package)
			{
				if (package.IsSpecial)
					return false;

				if (!string.IsNullOrEmpty(freeText) && package.Text.IndexOf(freeText) < 0)
					return false;

				if (selectedStatusIDs != null && !selectedStatusIDs.Contains(package.CurrentStatusID))
					return false;

				
				if (selectedCategoryIDs != null)
				{
					bool isInCategories = false;

					foreach (int categoryID in selectedCategoryIDs)
					{
						if (package.Categories.FirstOrDefault(c => c.PackageCategoryID == categoryID) != null)
						{
							isInCategories = true;
							break;
						}
					}

					if (!isInCategories)
						return false;
				}

				return true;
			});

			return from package in packages
				   where predicate(package)
				   select package;
		}

		public void PackageAction(IEnumerable<int> packageIDs, int actionID)
		{
			if (packageIDs == null)
				throw new ArgumentNullException();

			if (!Enum.IsDefined(typeof(PackageActionIndex), actionID))
				throw new ArgumentException();

			IEnumerable<PackageInfo> packages = PackageInfos;
			PackageActionIndex action = (PackageActionIndex)actionID;

			switch (action)
			{
				case PackageActionIndex.Install:
				case PackageActionIndex.Remove:
				case PackageActionIndex.Update:
					{
						foreach (int packageID in packageIDs)
						{
							PackageInfo package = GetPackageByID(packageID);
							PackageStatusIndex status = (PackageStatusIndex)package.StatusID;
							switch (action)
							{
								case PackageActionIndex.Install:
									switch (status)
									{
										case PackageStatusIndex.Available:
											package.CurrentStatusID = (int)PackageStatusIndex.MarkedForInstallation;

											IEnumerable<int> depencencyIDs = from dep in GetPackageDependencies(packageID, true)
																			 select dep.PackageID;

											// Do the same for all dependencies (and dependency of dependencies).
											PackageAction(depencencyIDs, actionID);
											break;
									}
									break;
								case PackageActionIndex.Remove:
									switch (status)
									{
										case PackageStatusIndex.InstalledOutdated:
										case PackageStatusIndex.InstalledUpdated:
											package.CurrentStatusID = (int)PackageStatusIndex.MarkedForRemoval;
											break;
									}
									break;
								case PackageActionIndex.Update:
									switch (status)
									{
										case PackageStatusIndex.InstalledOutdated:
											package.CurrentStatusID = (int)PackageStatusIndex.MarkedForUpdate;
											break;
									}
									break;
							}
						}
					}
					break;
				case PackageActionIndex.DependenciesInstall:
				case PackageActionIndex.DependenciesRemove:
				case PackageActionIndex.DependenciesUpdate:
					{
						IEnumerable<int> dependencyIDs = from dep in GetPackagesDependencies(packageIDs, true)
														 select dep.PackageID;
						switch (action)
						{
							case PackageActionIndex.DependenciesInstall:
								PackageAction(dependencyIDs, (int)PackageActionIndex.Install);
								break;
							case PackageActionIndex.DependenciesRemove:
								PackageAction(dependencyIDs, (int)PackageActionIndex.Remove);
								break;
							case PackageActionIndex.DependenciesUpdate:
								PackageAction(dependencyIDs, (int)PackageActionIndex.Update);
								break;
						}
					}
					break;
				default:
					throw new NotImplementedException();
			}

		}

		public IEnumerable<PackageInfo> GetPackagesActionAdditionalPackages(IEnumerable<int> packageIDs, IEnumerable<int> markedPackageIDs, int actionID)
		{
			if (packageIDs == null)
				throw new ArgumentNullException();

			if (markedPackageIDs == null)
				throw new ArgumentNullException();

			if (!Enum.IsDefined(typeof(PackageActionIndex), actionID))
				throw new ArgumentException();


			List<PackageInfo> packagesAdditionalPackages = new List<PackageInfo>();
			foreach (int packageID in packageIDs)
			{
				IEnumerable<PackageInfo> packageActionAdditionalPackages = GetPackageActionAdditionalPackages(packageID, markedPackageIDs, actionID);
				packagesAdditionalPackages.AddRange(packageActionAdditionalPackages);
			}
			return packagesAdditionalPackages;

		}

		public IEnumerable<PackageInfo> GetPackageActionAdditionalPackages(int packageID, IEnumerable<int> markedPackageIDs, int actionID)
		{
			if (packageID < 1)
				throw new ArgumentException();

			if (markedPackageIDs == null)
				throw new ArgumentNullException();

			if (!Enum.IsDefined(typeof(PackageActionIndex), actionID))
				throw new ArgumentException();

			List<PackageInfo> additionalPackages = new List<PackageInfo>();

			switch ((PackageActionIndex)actionID)
			{
				case PackageActionIndex.Install:
					foreach (PackageInfo dependency in GetPackageDependencies(packageID, true))
					{
						int dependencyID = dependency.PackageID;

						bool isInstalled = InstalledPackages.SingleOrDefault(p => p.PackageID == dependencyID) != null;
						if (isInstalled)
							continue;

						bool isMarked = markedPackageIDs.Contains(dependencyID);
						if (isMarked)
							continue;

						additionalPackages.Add(dependency);

						IEnumerable<PackageInfo> dependencyActionAdditionalPackages = GetPackageActionAdditionalPackages(dependencyID, markedPackageIDs, actionID);
						additionalPackages.AddRange(dependencyActionAdditionalPackages);
					}
					break;
			}

			return additionalPackages;
		}

		private IEnumerable<PackageInfo> GetPackageDependencies(int packageID, bool includeSpecial)
		{
			if (packageID < 1)
				throw new ArgumentException();

			PackageInfo package = GetPackageByID(packageID);
			IEnumerable<PackageInfo> dependencies = from dep in package.Requires
													where includeSpecial || !dep.IsSpecial
													select dep;

			return dependencies;
		}

		public IEnumerable<PackageInfo> GetPackagesDependencies(IEnumerable<int> packageIDs, bool includeSpecial)
		{
			if (packageIDs == null)
				throw new ArgumentException();

			IEnumerable<PackageInfo> dependencies = GetPackagesByID(packageIDs).
				SelectMany(p => p.Requires).
				Where(p => includeSpecial || !p.IsSpecial).
				Distinct();

			return dependencies;
		}

		public void Reset()
		{
			AvailablePackages = null;
			PackageCategories = null;

			// PackageStatuses and InstalledPackages do not change.
		}

		public void Apply()
		{
			if (PackagesNotMeetingRequirements.FirstOrDefault() != null)
				throw new InvalidOperationException();
		}

		public IEnumerable<PackageInfo> PackagesNotMeetingRequirements
		{
			get
			{
				IEnumerable<PackageInfo> packages = PackageInfos;
				if (packages == null)
					throw new InvalidOperationException();

				foreach (PackageInfo package in packages)
				{
					switch ((PackageStatusIndex)package.StatusID)
					{
						case PackageStatusIndex.Available: // Original package status was available.
							switch ((PackageStatusIndex)package.CurrentStatusID)
							{
								case PackageStatusIndex.MarkedForInstallation: // Now it's marked for installation.
									foreach (PackageInfo dependency in GetPackageDependencies(package.PackageID, true))
									{
										bool missing = false;
										switch ((PackageStatusIndex)dependency.StatusID)
										{
											case PackageStatusIndex.Available: // This requirement's original status was available.
												switch ((PackageStatusIndex)dependency.CurrentStatusID)
												{
													case PackageStatusIndex.MarkedForInstallation:
														// Ok, it's marked for installation.
														break;
													default:
														// It's not marked for installation.
														missing = true;
														break;
												}
												break;
										}

										if (missing)
											yield return package;
									}
									break;
							}
							break;
					}
				}
			}
		}
		#endregion

		#region Events
		public event EventHandler ApplyCompleted;
		protected virtual void OnApplyCompleted(EventArgs e)
		{
			if (ApplyCompleted != null)
				ApplyCompleted(this, e);
		}

		public event EventHandler<ApplyProgressChangedEventArgs> ApplyProgressChanged;
		protected virtual void OnApplyProgressChanged(ApplyProgressChangedEventArgs e)
		{
			if (ApplyProgressChanged != null)
				ApplyProgressChanged(this, e);
		}
		#endregion
	}
}
